package org.dreamwork.tools.code.analyzer.jfx.js;

import netscape.javascript.JSObject;
import org.dreamwork.util.ReferenceUtil;
import org.dreamwork.util.StringUtil;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;

public class JavaScriptShuttle {
    public static Object transform (Object raw) {
        if (raw instanceof JSObject) {
            JSObject o = (JSObject) raw;
            Boolean flag = (Boolean) o.eval ("this instanceof Date");
            if (flag != null && flag) { // js date object
                long timestamp = ((Number) o.eval ("this.getTime ()")).longValue ();
                return new Date (timestamp);
            } else {
                flag = (Boolean) o.eval ("this instanceof Array");
                if (flag != null && flag) { // js array
                    int length = ((Number) o.eval ("this.length")).intValue ();
                    Object[] a = new Object[length];
                    for (int i = 0; i < length; i ++) {
                        a[i] = transform (o.eval ("this[" + i + "]"));
                    }
                    return a;
                }
            }
        }
        return raw;
    }

    public static<T> T from (JSObject obj, Class<T> type) {
        try {
            T bean = type.newInstance ();
            Collection<Field> fields = getAllAnnotatedFields (type);
            for (Field field : fields) {
                JSField js = field.getAnnotation (JSField.class);
                String name = js.value ();
                if (StringUtil.isEmpty (name)) {
                    name = field.getName ();
                }
                Object o = obj.getMember (name);
                if (o != null) {
                    Class<?> ft = field.getType ();
                    Class<?> pt = o.getClass ();
                    if (ft.isAssignableFrom (pt)) {
                        field.set (bean, o);
                    } else if (o instanceof JSObject) {
                        o = _transform (o);
                        pt = o.getClass ();
                        if (ft.isAssignableFrom (pt)) {
                            field.set (bean, o);
                        } else if (o instanceof JSObject) {
                            o = from ((JSObject) o, ft);
                            field.set (bean, o);
                        }
                    }
                }
            }

            Collection<Method> methods = getAllAnnotatedMethod (type);
            for (Method method : methods) {
                JSField js = method.getAnnotation (JSField.class);
                String name = js.value ();
                if (StringUtil.isEmpty (name)) {
                    name = method.getName ();
                    name = Character.toLowerCase (name.charAt (3)) + name.substring (4);
                }
                Object o = obj.getMember (name);
                if (o != null) {
                    Class<?> ft = method.getReturnType ();
                    Class<?> pt = o.getClass ();
                    if (ft.isAssignableFrom (pt)) {
                        method.invoke (bean, o);
                    } else if (o instanceof JSObject) {
                        o = _transform (o);
                        pt = o.getClass ();
                        if (ft.isAssignableFrom (pt)) {
                            method.invoke (bean, o);
                        } else if (o instanceof JSObject) {
                            o = from ((JSObject) o, ft);
                            method.invoke (bean, o);
                        }
                    }
                }
            }
            return bean;
        } catch (Exception ex) {
            throw new RuntimeException (ex);
        }
    }

    public static Object to (Object bean, JSObject window) {
        if (bean == null) return null;
        Class<?> type = bean.getClass ();
        if (Date.class.isAssignableFrom (type)) {
            return window.eval ("new Date (" + ((Date) bean).getTime () + ")");
        }
        if (type.isArray ()) {
            JSObject a = (JSObject) window.eval ("[]");
            int length = Array.getLength (bean);
            for (int i = 0; i < length; i ++) {
                a.setSlot (i, to (Array.get (bean, i), window));
            }
        }

        return bean;
    }

    private static Collection<Field> getAllAnnotatedFields (Class<?> type) {
        Set<Field> set = new HashSet<> ();
        Class<?> p = type;
        while (p != null && p != Object.class) {
            Arrays.stream (p.getDeclaredFields ()).forEach (f -> {
                if (f.isAnnotationPresent (JSField.class)) {
                    f.setAccessible (true);
                    set.add (f);
                }
            });
            p = p.getSuperclass ();
        }
        return set;
    }

    private static Collection<Method> getAllAnnotatedMethod (Class<?> type) {
        Set<Method> set = new HashSet<> ();
        Class<?> p = type;
        while (p != null && p != Object.class) {
            Arrays.stream (p.getDeclaredMethods ()).forEach (m -> {
                if (m.isAnnotationPresent (JSField.class)) {
                    String name = m.getName ();
                    if (name.startsWith ("set") && m.getParameterCount () == 1) {
                        m.setAccessible (true);
                        set.add (m);
                    }
                }
            });
            p = p.getSuperclass ();
        }
        return set;
    }

    private static Object _transform (Object raw) {
        if (raw == null) return null;
        Class<?> type = raw.getClass ();
        if (ReferenceUtil.isKnownType (type)) {
            return raw;
        }
        if (raw instanceof CharSequence) return raw;

        return transform (raw);
    }

    private static Object createInstance (Class<?> type, JSField field) throws InstantiationException, IllegalAccessException {
        if (field == null) {
            if (type.isInterface ()) {
                if (Map.class.isAssignableFrom (type)) {
                    return new HashMap<> ();
                } else if (List.class.isAssignableFrom (type)) {
                    return new ArrayList<> ();
                } else if (Collection.class.isAssignableFrom (type)) {
                    return new HashSet<> ();
                }
            } else if (type.isArray ()) {

            }
        }
        return type.newInstance ();
    }
}